package scaladuct

import _root_.util.GenericAnalysisUtils
import _root_.util.logging.Logged
import java.net.URL
import java.lang.{Class, String}
import net.sf.cglib.core.{Predicate, NamingPolicy}
import org.objectweb.asm._
import java.io.InputStream
import net.sf.cglib.beans.BeanGenerator
import signature.SignatureWriter
import collection.mutable.{HashMap, Map}

/**
 * <p>
 * Created by IntelliJ IDEA.
 * User: zslajchrt
 * Date: Aug 11, 2010
 * Time: 5:48:13 PM
 */

object ExpressCookieClassLoader {
  private[scaladuct] val byteCodeCache = new HashMap[String, Array[Byte]]
}

class ExpressCookieClassLoader(val methodName: String,
                               val metaCookie: MetaCookie,
                               val cookieClassName: String,
                               val starterClassResource: URL,
                               val starterClass: Class[_])
  extends ClassLoader(Thread.currentThread().getContextClassLoader()) with Logged {
  def loadCookieClass(): Class[ExpressCookie] =
    loadClass(cookieClassName).asInstanceOf[Class[ExpressCookie]]

  def loadContextClass(): Class[ExpressCookie] =
    loadClass(cookieClassName + "Context").asInstanceOf[Class[ExpressCookie]]

  lazy val cookieByteCode = {
    ExpressCookieClassLoader.byteCodeCache.synchronized {
      if (ExpressCookieClassLoader.byteCodeCache.contains(cookieClassName)) {
        ExpressCookieClassLoader.byteCodeCache.apply(cookieClassName)
      } else {
        val byteCode = initializeCookieGenerator(metaCookie.cookieProperties, cookieClassName, metaCookie.firstArgumentIndex)
        ExpressCookieClassLoader.byteCodeCache.put(cookieClassName, byteCode)
        byteCode
      }
    }
  }

  lazy val contextByteCode = {
    val extendedName: String = cookieClassName + "Context"
    ExpressCookieClassLoader.byteCodeCache.synchronized {
      if (ExpressCookieClassLoader.byteCodeCache.contains(extendedName)) {
        ExpressCookieClassLoader.byteCodeCache.apply(extendedName)
      } else {
        val byteCode = initializeCookieGenerator(metaCookie.contextProperties, extendedName, 0)
        ExpressCookieClassLoader.byteCodeCache.put(extendedName, byteCode)
        byteCode
      }
    }
  }

  protected override def findClass(className: String): Class[_] = {
    val byteCode =
      if (className == cookieClassName)
      //initializeCookieGenerator(metaCookie.cookieProperties, className, metaCookie.firstArgumentIndex)
        cookieByteCode
      else if (className == cookieClassName + "Context")
      //initializeCookieGenerator(metaCookie.contextProperties, className, 0)
        contextByteCode
      else
        null

    if (byteCode != null)
      defineClass(className, byteCode, 0, byteCode.length);
    else
      null
  }

  private def initializeCookieGenerator(cookieProperties: List[PropertyValue[Object]],
                                        className: String,
                                        firstPropertyOffset: Int): Array[Byte] = {
    val cookieGenerator = new BeanGenerator();
    cookieGenerator.setSuperclass(classOf[ExpressCookie]);

    var propertiesOrder: Map[String, Int] = Map.empty
    cookieProperties.foreach(cookieProperty => {
      // Define the cookie's property
      // Ignore references that are associated with this ref property
      if (cookieProperty.refTypeInfo != null)
      // Change the type of the property to either String or List[String] as long as it is a reference.
      // In such a case the client must provide the name of the reference as the value of the reference property.
      // The named reference will be resolved when the cookie containing that property is consumed.
        if (classOf[java.util.Collection[_]].isAssignableFrom(cookieProperty.clazz) && cookieProperty.refTypeInfo.collection) {
          // vector of reference
          cookieGenerator.addProperty(cookieProperty.name, classOf[java.util.List[String]])
        } else {
          // scalar reference
          cookieGenerator.addProperty(cookieProperty.name, classOf[String])
        }
      else
        cookieGenerator.addProperty(cookieProperty.name, cookieProperty.clazz)
      propertiesOrder += (("get" + cookieProperty.name).toLowerCase -> (firstPropertyOffset + propertiesOrder.size))
    })

    cookieGenerator.setNamingPolicy(new NamingPolicy() {
      def getClassName(prefix: String, source: String, key: Object, names: Predicate): String = className
    })

    debug("Generating cookie class " + className + " (loader:" + this + ")");

    return generateCookieClass(cookieGenerator, propertiesOrder);
  }

  private def getPropertyFromGetter(getterName: String): ArgPropertyValue[_] = {
    val propName = getterName.substring(3)
    metaCookie.property(propName) match {
      case Some(prop) => prop
      case None => null
    }
  }

  private def generateCookieClass(cookieGenerator: BeanGenerator, propertiesOrder: Map[String, Int]): Array[Byte] = {
    cookieGenerator.setUseCache(false)
    cookieGenerator.createClass()
    val classWriter = new ClassWriter(1)
    cookieGenerator.generateClass(new CookieClassWriter)

    class CookieClassWriter
      extends ClassAdapter(classWriter) {
      override def visit(version: Int, access: Int, name: String, signature: String, superName: String, interfaces: Array[String]): Unit =
        super.visit(Opcodes.V1_5, access, name, signature, superName, interfaces)

      override def visitMethod(access: Int, name: String, desc: String, signature: String, exceptions: Array[String]): MethodVisitor = {
        // generate the correct signature for properties of generic type
        val actualSignature = if (name.startsWith("get")) {
          val propName = name.substring(3)
          metaCookie.property(propName) match {
            case Some(prop) => {
              // Emit the generic signature for everything except scalar references. For scalar references the
              // generic type is not so important.
              // For vector references we need the String generic parameter in the getter's return type
              // for the sake of AuxConverter to discern the collection's element type (which is String).
              if (prop.refTypeInfo == null || prop.refTypeInfo.collection) {
                // Generate a generic signature for non-references only, because the type of reference properties
                // is changed to String. The genericParameters attribute relates to the original type.
                try {
                  val genParams = GenericAnalysisUtils.getParameterTypes(prop.typeInfo.getType)
                  genericGetterSignature(desc, genParams)
                } catch {
                  case _ => signature
                }
              } else signature
            }
            case _ => signature
          }
        } else if (name.startsWith("set")) {
          val propName = name.substring(3)
          metaCookie.property(propName) match {
            case Some(prop) => {
              // See above
              if (prop.refTypeInfo == null || prop.refTypeInfo.collection) {
                try {
                  val genParams = GenericAnalysisUtils.getParameterTypes(prop.typeInfo.getType)
                  genericSetterSignature(desc, genParams)
                } catch {
                  case _ => signature
                }
              } else signature
            }
            case _ => signature
          }
        } else
          signature

        val getterVisitor = super.visitMethod(access, name, desc, actualSignature, exceptions);

        if (name.startsWith("get")) {
          // Assume ordering of the getters matches the ordering of the parameters
          val getterIndex = propertiesOrder(name.toLowerCase)

          /**
           * The recursive method for copying annotations from the phase method on the starter class to the
           * getters in the cookie created from the phase method's arguments.
           */
          def copyAnnotations(classAsStream: InputStream): Unit = {
            val classReader = new ClassReader(classAsStream)

            val prop = getPropertyFromGetter(name)

            val isRefCollection = classOf[java.util.Collection[_]].isAssignableFrom(prop.clazz) &&
              prop.refTypeInfo != null && prop.refTypeInfo.collection

            val copyAnnotationsVisitor = new CopyAnnotationsVisitor(getterIndex, getterVisitor, methodName,
              starterClass, methodName, prop.argIndex, isRefCollection)

            classReader.accept(copyAnnotationsVisitor, 1)
            if (!copyAnnotationsVisitor.phaseMethodVisited) {
              // attempt to find the corresponding phase method in the super-class
              val superClassName = copyAnnotationsVisitor.superClassName
              if (superClassName == null) {
                throw new IllegalStateException("Cannot copy annotations from " + name + " method");
              }
              val superClassAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(superClassName + ".class")
              copyAnnotations(superClassAsStream)
            }

          }

          copyAnnotations(starterClassResource.openStream())
        }

        getterVisitor;
      }

    }

    classWriter.toByteArray()

  }


  //  def argumentTypeDescriptors(genType: java.lang.reflect.Type): Option[Array[String]] = {
  //    genType match {
  //      case parGenType: ParameterizedType => {
  //        if (parGenType.getActualTypeArguments.forall(_.isInstanceOf[Class[_]])) {
  //          // at this moment only (invariant) class arguments are supported
  //          val argDescs: Array[String] = parGenType.getActualTypeArguments.map((t) => {
  //            val c = t.asInstanceOf[Class[_]]
  //            c.getName.replace('.', '/')
  //          })
  //          Some(argDescs)
  //        } else
  //          None
  //      }
  //      case _ => None
  //    }
  //  }

  def argumentTypeDescriptors(genericParams: Array[java.lang.reflect.Type]): Array[String] = {
    val argDescs: Array[String] = genericParams.map((genParam) => {
      GenericAnalysisUtils.getRawClassFromType(genParam).getName.replace('.', '/')
    })
    argDescs
  }

  def genericSetterSignature(desc: String, genericParams: Array[java.lang.reflect.Type]): String = {
    def getSignature(argDescs: Array[String]): String = {
      val wr = new SignatureWriter();
      val argumentVisitor = wr.visitParameterType();
      val argClsName: String = desc.substring(2, desc.length - 3)
      argumentVisitor.visitClassType(argClsName);

      argDescs.foreach((a) => {
        argumentVisitor.visitTypeArgument('=');
        argumentVisitor.visitClassType(a);
        argumentVisitor.visitEnd();
      })

      argumentVisitor.visitEnd();

      val returnTypeVisitor = wr.visitReturnType();
      returnTypeVisitor.visitBaseType('V');

      wr.toString
    }

    getSignature(argumentTypeDescriptors(genericParams.map(GenericAnalysisUtils.getRawClassFromType(_))))

  }

  def genericGetterSignature(desc: String, genericParams: Array[java.lang.reflect.Type]): String = {

    def getSignature(argDescs: Array[String]): String = {
      val wr = new SignatureWriter();
      val returnTypeVisitor = wr.visitReturnType();
      val retClsName: String = desc.substring(3, desc.length - 1)
      returnTypeVisitor.visitClassType(retClsName);

      argDescs.foreach((a) => {
        val argVis = returnTypeVisitor.visitTypeArgument('=');
        argVis.visitClassType(a);
        argVis.visitEnd();
      })

      returnTypeVisitor.visitEnd();

      wr.toString
    }

    getSignature(argumentTypeDescriptors(genericParams.map(GenericAnalysisUtils.getRawClassFromType(_))))
  }

}